<template>
  <div :class="prefixCls" ref="treeWrap">
    <TreeNode
      v-for="(item, i) in stateTree"
      :key="i"
      :data="item"
      visible
      :multiple="multiple"
      :show-checkbox="showCheckbox"
      :children-key="childrenKey"
    >
    </TreeNode>
    <div :class="[prefixCls + '-empty']" v-if="!stateTree.length">
      {{ localeEmptyText }}
    </div>
    <div class="haloe-tree-context-menu" :style="contextMenuStyles">
      <Dropdown
        trigger="custom"
        :visible="contextMenuVisible"
        transfer
        @on-click="handleClickDropdownItem"
        @on-clickoutside="handleClickContextMenuOutside"
      >
        <template #list>
          <DropdownMenu>
            <slot name="contextMenu"></slot>
          </DropdownMenu>
        </template>
      </Dropdown>
    </div>
  </div>
</template>
<script>
import { nextTick } from 'vue'
import TreeNode from './node.vue'
import Dropdown from '../dropdown/dropdown.vue'
import DropdownMenu from '../dropdown/dropdown-menu.vue'
import Locale from '../../mixins/locale'

const prefixCls = 'haloe-tree'

export default {
  name: 'Tree',
  mixins: [Locale],
  components: { TreeNode, Dropdown, DropdownMenu },
  emits: [
    'on-select-change',
    'on-check-change',
    'on-contextmenu',
    'on-toggle-expand',
  ],
  provide() {
    return {
      TreeInstance: this,
    }
  },
  props: {
    data: {
      type: Array,
      default: () => [],
    },
    multiple: {
      type: Boolean,
      default: false,
    },
    showCheckbox: {
      type: Boolean,
      default: false,
    },
    checkStrictly: {
      type: Boolean,
      default: false,
    },
    // 当开启 showCheckbox 时，如果开启 checkDirectly，select 将强制转为 check 事件
    checkDirectly: {
      type: Boolean,
      default: false,
    },
    emptyText: {
      type: String,
    },
    childrenKey: {
      type: String,
      default: 'children',
    },
    loadData: {
      type: Function,
    },
    render: {
      type: Function,
    },
    selectNode: {
      type: Boolean,
      default: true,
    },
    expandNode: {
      type: Boolean,
      default: false,
    },
    autoCloseContextmenu: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      prefixCls: prefixCls,
      stateTree: this.data,
      flatState: [],
      contextMenuVisible: false,
      contextMenuStyles: {
        top: 0,
        left: 0,
      },
    }
  },
  watch: {
    data: {
      deep: true,
      handler() {
        this.stateTree = this.data
        this.flatState = this.compileFlatState()
        this.rebuildTree()
      },
    },
  },
  computed: {
    localeEmptyText() {
      if (typeof this.emptyText === 'undefined') {
        return this.t('i.tree.emptyText')
      } else {
        return this.emptyText
      }
    },
  },
  methods: {
    compileFlatState() {
      // so we have always a relation parent/children of each node
      let keyCounter = 0
      let childrenKey = this.childrenKey
      const flatTree = []
      function flattenChildren(node, parent) {
        node.nodeKey = keyCounter++
        flatTree[node.nodeKey] = { node: node, nodeKey: node.nodeKey }
        if (typeof parent != 'undefined') {
          flatTree[node.nodeKey].parent = parent.nodeKey
          flatTree[parent.nodeKey][childrenKey].push(node.nodeKey)
        }

        if (node[childrenKey]) {
          flatTree[node.nodeKey][childrenKey] = []
          node[childrenKey].forEach((child) => flattenChildren(child, node))
        }
      }
      this.stateTree.forEach((rootNode) => {
        flattenChildren(rootNode)
      })
      return flatTree
    },
    updateTreeUp(nodeKey) {
      const parentKey = this.flatState[nodeKey].parent
      if (typeof parentKey == 'undefined' || this.checkStrictly) return

      const node = this.flatState[nodeKey].node
      const parent = this.flatState[parentKey].node
      if (
        node.checked == parent.checked &&
        node.indeterminate == parent.indeterminate
      )
        return // no need to update upwards

      if (node.checked == true) {
        parent.checked = parent[this.childrenKey].every((node) => node.checked)
        parent.indeterminate = !parent.checked
      } else {
        parent.checked = false
        parent.indeterminate = parent[this.childrenKey].some(
          (node) => node.checked || node.indeterminate
        )
      }
      this.updateTreeUp(parentKey)
    },
    rebuildTree() {
      // only called when `data` prop changes
      const checkedNodes = this.getCheckedNodes()
      checkedNodes.forEach((node) => {
        this.updateTreeDown(node, { checked: true })
        // propagate upwards
        const parentKey = this.flatState[node.nodeKey].parent
        if (!parentKey && parentKey !== 0) return
        const parent = this.flatState[parentKey].node
        const childHasCheckSetter =
          typeof node.checked != 'undefined' && node.checked
        if (childHasCheckSetter && parent.checked != node.checked) {
          this.updateTreeUp(node.nodeKey) // update tree upwards
        }
      })
    },

    getSelectedNodes() {
      /* public API */
      return this.flatState
        .filter((obj) => obj.node.selected)
        .map((obj) => obj.node)
    },
    getCheckedNodes() {
      /* public API */
      return this.flatState
        .filter((obj) => obj.node.checked)
        .map((obj) => obj.node)
    },
    getCheckedAndIndeterminateNodes() {
      /* public API */
      return this.flatState
        .filter((obj) => obj.node.checked || obj.node.indeterminate)
        .map((obj) => obj.node)
    },
    updateTreeDown(node, changes = {}) {
      if (this.checkStrictly) return

      for (let key in changes) {
        node[key] = changes[key]
      }
      if (node[this.childrenKey]) {
        node[this.childrenKey].forEach((child) => {
          this.updateTreeDown(child, changes)
        })
      }
    },
    handleSelect(nodeKey) {
      if (!this.flatState[nodeKey]) return
      const node = this.flatState[nodeKey].node
      if (!this.multiple) {
        // reset previously selected node
        const currentSelectedKey = this.flatState.findIndex(
          (obj) => obj.node.selected
        )
        if (currentSelectedKey >= 0 && currentSelectedKey !== nodeKey)
          this.flatState[currentSelectedKey].node.selected = false
      }
      node.selected = !node.selected

      this.$emit('on-select-change', this.getSelectedNodes(), node)
    },
    handleCheck({ checked, nodeKey }) {
      if (!this.flatState[nodeKey]) return
      const node = this.flatState[nodeKey].node
      node.checked = checked
      node.indeterminate = false

      this.updateTreeUp(nodeKey) // propagate up
      this.updateTreeDown(node, { checked, indeterminate: false }) // reset `indeterminate` when going down

      this.$emit('on-check-change', this.getCheckedNodes(), node)
    },
    handleContextmenu({ data, event }) {
      if (this.contextMenuVisible) this.handleClickContextMenuOutside()
      nextTick(() => {
        const $TreeWrap = this.$refs.treeWrap
        const TreeBounding = $TreeWrap.getBoundingClientRect()
        const position = {
          left: `${event.clientX - TreeBounding.left}px`,
          top: `${event.clientY - TreeBounding.top}px`,
        }
        this.contextMenuStyles = position
        this.contextMenuVisible = true
        this.$emit('on-contextmenu', data, event, position)
      })
    },
    handleClickContextMenuOutside() {
      this.contextMenuVisible = false
    },
    handleOnCheck(param) {
      this.handleCheck(param)
    },
    handleOnSelected(param) {
      this.handleSelect(param)
    },
    handleToggleExpand(node) {
      this.$emit('on-toggle-expand', node)
    },
    handleOnContextmenu(param) {
      this.handleContextmenu(param)
    },
    closeContextMenu() {
      this.handleClickContextMenuOutside()
    },
    handleClickDropdownItem() {
      if (this.autoCloseContextmenu) this.closeContextMenu()
    },
  },
  created() {
    this.flatState = this.compileFlatState()
    this.rebuildTree()
  },
}
</script>
